Skip to main content
Glama
northernvariables

FedMCP - Federal Parliamentary Information

page.tsx18.6 kB
/** * Dashboard page - Enhanced overview of government activity * Fully bilingual with Quebec French support */ 'use client'; import { useState } from 'react'; import { useQuery } from '@apollo/client'; import { useTranslations, useLocale } from 'next-intl'; import { Header } from '@/components/Header'; import { Footer } from '@/components/Footer'; import { Loading } from '@/components/Loading'; import { Card } from '@canadagpt/design-system'; import { StatCard } from '@/components/dashboard/StatCard'; import { GET_TOP_SPENDERS, SEARCH_MPS, SEARCH_BILLS, GET_RECENT_STATEMENTS, GET_DASHBOARD_COUNTS, GET_RANDOM_MPS, GET_DASHBOARD_LOBBYING } from '@/lib/queries'; import { Link } from '@/i18n/navigation'; import { formatCAD } from '@canadagpt/design-system'; import { Users, FileText, Megaphone, DollarSign, TrendingUp, MessageSquare, Info, Building } from 'lucide-react'; import { CompactMPCard } from '@/components/MPCard'; import { CompactPartyFilterButtons } from '@/components/PartyFilterButtons'; import { getBilingualContent } from '@/hooks/useBilingual'; import { format } from 'date-fns'; import { fr, enUS } from 'date-fns/locale'; export default function DashboardPage() { const t = useTranslations('dashboard'); const locale = useLocale(); const dateLocale = locale === 'fr' ? fr : enUS; // Use FY 2026 - expense data typically lags 2-3 months after quarter end const fiscalYear = 2026; // Party filter state - array for multi-select const [partyFilter, setPartyFilter] = useState<string[]>([]); const { data: spendersData, loading: spendersLoading } = useQuery(GET_TOP_SPENDERS, { variables: { fiscalYear, limit: 10 }, }); // Get recent statements (last 30 days) for the metric and tile const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); const { data: recentStatementsData, loading: statementsLoading } = useQuery(GET_RECENT_STATEMENTS, { variables: { limit: 100 }, // Fetch enough to filter by date }); const { data: lobbyingData, loading: lobbyingLoading } = useQuery(GET_DASHBOARD_LOBBYING); // Optimized: Get counts with aggregate queries - 98% less data transfer const { data: countsData } = useQuery(GET_DASHBOARD_COUNTS); // Optimized: Get active bills count (need this separate as aggregate doesn't support complex filters yet) const { data: activeBillsData } = useQuery(SEARCH_BILLS, { variables: { limit: 1000 }, // TODO: Add server-side active filter }); // Optimized: Server-side randomized MPs with party filtering - no client-side shuffle needed const { data: featuredMPsData, loading: featuredMPsLoading } = useQuery(GET_RANDOM_MPS, { variables: { parties: partyFilter.length > 0 ? partyFilter : null, limit: 8 }, }); const totalMPs = countsData?.mpsAggregate?.count || 343; const totalBills = countsData?.billsAggregate?.count || 0; const activeBills = activeBillsData?.searchBills?.filter( (b: any) => b.title && !['Passed', 'Royal Assent'].includes(b.status) )?.length || 0; // Filter speeches from the last 30 days const recentSpeeches = recentStatementsData?.statements?.filter((statement: any) => { if (!statement.partOf?.date) return false; const statementDate = new Date(statement.partOf.date); return statementDate >= thirtyDaysAgo; }) || []; const recentSpeechCount = recentSpeeches.length; return ( <div className="min-h-screen flex flex-col"> <Header /> <main className="flex-1 page-container"> <h1 className="text-4xl font-bold text-text-primary mb-2">{t('title')}</h1> <p className="text-text-secondary mb-8">{t('subtitle')}</p> {/* Key Metrics Row */} <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8"> <StatCard title={t('metrics.currentMPs')} value={totalMPs} icon={Users} subtitle={t('metrics.membersOfParliament')} href="/mps" /> <StatCard title={t('metrics.totalBills')} value={totalBills} icon={FileText} subtitle={t('metrics.activeBills', { count: activeBills })} href="/bills" /> <StatCard title={t('metrics.topSpender')} value={spendersData?.topSpenders?.[0] ? formatCAD(spendersData.topSpenders[0].total_expenses, { compact: true }) : '—'} icon={DollarSign} subtitle={t('metrics.expenses', { year: fiscalYear })} /> <StatCard title={t('metrics.recentSpeeches')} value={recentSpeechCount} icon={MessageSquare} subtitle={t('metrics.last30Days')} href="/hansard" /> </div> {/* Quick Actions */} <div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8"> <Link href="/mps" className="group"> <Card className="hover:border-accent-red transition-colors cursor-pointer text-center p-6"> <Users className="h-8 w-8 text-accent-red mx-auto mb-2" /> <h3 className="font-semibold text-text-primary group-hover:text-accent-red transition-colors"> {t('quickActions.browseMPs')} </h3> <p className="text-sm text-text-secondary mt-1">{t('quickActions.viewAllMembers')}</p> </Card> </Link> <Link href="/bills" className="group"> <Card className="hover:border-accent-red transition-colors cursor-pointer text-center p-6"> <FileText className="h-8 w-8 text-accent-red mx-auto mb-2" /> <h3 className="font-semibold text-text-primary group-hover:text-accent-red transition-colors"> {t('quickActions.trackBills')} </h3> <p className="text-sm text-text-secondary mt-1">{t('quickActions.followLegislation')}</p> </Card> </Link> <Link href="/lobbying" className="group"> <Card className="hover:border-accent-red transition-colors cursor-pointer text-center p-6"> <Megaphone className="h-8 w-8 text-accent-red mx-auto mb-2" /> <h3 className="font-semibold text-text-primary group-hover:text-accent-red transition-colors"> {t('quickActions.lobbying')} </h3> <p className="text-sm text-text-secondary mt-1">{t('quickActions.corporateInfluence')}</p> </Card> </Link> <Link href={`/spending?year=${fiscalYear}` as any} className="group"> <Card className="hover:border-accent-red transition-colors cursor-pointer text-center p-6"> <DollarSign className="h-8 w-8 text-accent-red mx-auto mb-2" /> <h3 className="font-semibold text-text-primary group-hover:text-accent-red transition-colors"> {t('quickActions.spending')} </h3> <p className="text-sm text-text-secondary mt-1">{t('quickActions.mpExpenses')}</p> </Card> </Link> </div> {/* Featured MPs Section */} <Card className="mb-8"> <div className="flex items-center justify-between mb-4"> <h2 className="text-2xl font-bold text-text-primary">{t('featuredMPs.title')}</h2> <Users className="h-6 w-6 text-accent-red" /> </div> {/* Party Filter Buttons */} <div className="mb-4"> <CompactPartyFilterButtons selected={partyFilter} onSelect={(parties) => setPartyFilter(parties)} /> </div> {/* MPs Grid */} {featuredMPsLoading ? ( <Loading /> ) : ( <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-4 gap-3"> {(featuredMPsData?.randomMPs || []).map((mp: any) => ( <CompactMPCard key={mp.id} mp={mp} /> ))} </div> )} <div className="mt-4 pt-4 border-t border-border-subtle"> <Link href="/mps" className="text-sm text-accent-red hover:text-accent-red-hover font-semibold" > {t('featuredMPs.viewAll')} </Link> </div> </Card> {/* Main Content Grid */} <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> {/* Top Spenders */} <Card> <div className="flex items-center justify-between mb-4"> <h2 className="text-2xl font-bold text-text-primary"> {t('topSpenders.title', { year: fiscalYear })} </h2> <TrendingUp className="h-6 w-6 text-accent-red" /> </div> {spendersLoading ? ( <Loading /> ) : ( <div className="space-y-3"> {spendersData?.topSpenders?.map((item: any, index: number) => ( <Link key={item.mp.id} href={`/mps/${item.mp.id}` as any} className="flex items-center justify-between p-3 rounded-lg hover:bg-bg-elevated transition-colors group" > <div className="flex items-center space-x-3"> <span className="text-2xl font-bold text-text-tertiary w-6"> {index + 1} </span> <div> <div className="font-semibold text-text-primary group-hover:text-accent-red transition-colors"> {item.mp.name} </div> <div className="text-sm text-text-secondary">{item.mp.party}</div> </div> </div> <div className="text-lg font-semibold text-accent-red"> {formatCAD(item.total_expenses, { compact: true })} </div> </Link> ))} </div> )} <div className="mt-4 pt-4 border-t border-border-subtle"> <Link href={`/spending?year=${fiscalYear}` as any} className="text-sm text-accent-red hover:text-accent-red-hover font-semibold" > {t('topSpenders.viewAll')} </Link> </div> </Card> {/* Recent Debates from Hansard */} <Card> <div className="flex items-center justify-between mb-4"> <h2 className="text-2xl font-bold text-text-primary"> {t('recentDebates.title')} </h2> <MessageSquare className="h-6 w-6 text-accent-red" /> </div> {statementsLoading ? ( <Loading /> ) : ( <div className="space-y-3"> {recentSpeeches.slice(0, 5).map((speech: any) => { const bilingualSpeech = getBilingualContent(speech, locale); return ( <div key={speech.id} className="p-3 rounded-lg bg-bg-elevated border border-border-subtle hover:border-accent-red/30 transition-colors" > <div className="flex items-center justify-between mb-2"> {speech.madeBy ? ( <Link href={`/mps/${speech.madeBy.id}` as any} className="font-semibold text-text-primary hover:text-accent-red transition-colors" > {speech.madeBy.name} </Link> ) : ( <span className="font-semibold text-text-primary"> {bilingualSpeech.who} </span> )} {speech.partOf?.date && ( <span className="text-sm text-text-tertiary"> {format(new Date(speech.partOf.date), 'PPP', { locale: dateLocale })} </span> )} </div> {bilingualSpeech.h2 && ( <div className="text-sm font-medium text-text-secondary mb-1"> {bilingualSpeech.h2} </div> )} <div className="text-sm text-text-secondary line-clamp-2"> {bilingualSpeech.content} </div> {speech.madeBy && ( <div className="text-xs text-text-tertiary mt-1"> {speech.madeBy.party} </div> )} </div> ); })} </div> )} <div className="mt-4 pt-4 border-t border-border-subtle"> <Link href="/hansard" className="text-sm text-accent-red hover:text-accent-red-hover font-semibold" > {t('recentDebates.searchHansard')} </Link> </div> </Card> </div> {/* Recent Lobbying Activity */} <Card className="mt-8"> <div className="flex items-center justify-between mb-4"> <h2 className="text-2xl font-bold text-text-primary"> {t('recentLobbying.title')} </h2> <Building className="h-6 w-6 text-accent-red" /> </div> {lobbyingLoading ? ( <Loading /> ) : lobbyingData?.lobbyCommunications && lobbyingData.lobbyCommunications.length > 0 ? ( <div className="space-y-3"> {lobbyingData.lobbyCommunications.slice(0, 8).map((comm: any) => { const orgName = comm.organization?.name || comm.client_org_name || 'Unknown Organization'; const lobbyistName = comm.lobbyist?.name || comm.registrant_name || 'Unknown Lobbyist'; const dpohNames = comm.dpoh_names?.slice(0, 2) || []; const subjectMatters = comm.subject_matters?.slice(0, 3) || []; return ( <div key={comm.id} className="p-3 rounded-lg bg-bg-elevated border border-border-subtle hover:border-accent-red/30 transition-colors" > <div className="flex items-start justify-between mb-2"> <div className="flex-1"> {comm.organization ? ( <Link href={`/organizations/${comm.organization.id}` as any} className="font-semibold text-text-primary hover:text-accent-red transition-colors mb-1 inline-block" > {orgName} </Link> ) : ( <div className="font-semibold text-text-primary mb-1"> {orgName} </div> )} <div className="text-sm text-text-secondary"> Lobbyist: {comm.lobbyist ? ( <Link href={`/lobbyists/${comm.lobbyist.id}` as any} className="text-accent-red hover:underline" > {lobbyistName} </Link> ) : ( lobbyistName )} </div> </div> {comm.date && ( <span className="text-sm text-text-tertiary whitespace-nowrap ml-4"> {format(new Date(comm.date), 'MMM d, yyyy', { locale: dateLocale })} </span> )} </div> {dpohNames.length > 0 && ( <div className="text-xs text-text-tertiary mb-2"> Met with: {dpohNames.join(', ')} {comm.dpoh_names && comm.dpoh_names.length > 2 && ` +${comm.dpoh_names.length - 2} more`} </div> )} {subjectMatters.length > 0 && ( <div className="flex flex-wrap gap-1 mt-2"> {subjectMatters.map((subject: string, idx: number) => ( <span key={idx} className="text-xs px-2 py-0.5 rounded bg-gray-500/20 text-gray-400" > {subject} </span> ))} {comm.subject_matters && comm.subject_matters.length > 3 && ( <span className="text-xs px-2 py-0.5 rounded bg-gray-500/20 text-gray-400"> +{comm.subject_matters.length - 3} </span> )} </div> )} </div> ); })} </div> ) : ( <p className="text-text-secondary">No recent lobbying activity found.</p> )} <div className="mt-4 pt-4 border-t border-border-subtle"> <Link href="/lobbying" className="text-sm text-accent-red hover:text-accent-red-hover font-semibold" > {t('recentLobbying.viewAll')} </Link> </div> </Card> {/* Information Banner */} <Card className="mt-8 bg-bg-overlay border-accent-red/20"> <div className="flex items-start gap-4"> <div className="p-2 bg-accent-red/10 rounded-lg"> <Info className="h-6 w-6 text-accent-red" /> </div> <div className="flex-1"> <h3 className="font-semibold text-text-primary mb-2"> {t('about.title')} </h3> <p className="text-sm text-text-secondary"> {t('about.description')} </p> </div> </div> </Card> </main> <Footer /> </div> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/northernvariables/FedMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server